package apig.server;

import java.sql.*;
import java.util.*;

import apii.algorithm.*;

/**
 * <b>Note:</b> Thread SAFE.
 * <p>
 * <b>Note:</b> Creates threads.
 * 
 * UNDER CONSTRUCTION
 * 
 * @author evgeniy
 * 
 */
public class AlgoRunManager {

	private static final int RUNNING = 1;
	private static final int FINISHED_OK = 2;
	private static final int FINISHED_FAIL = 3;

	private static AlgoRunManager instance;

	private final Map<Integer, CodeLabour> activeCodeLabours = new HashMap<Integer, CodeLabour>();
	private Algorithm[] algos;

	public boolean runCodeLabour(int codeId) throws SQLException {
		if (!isCodeChecked(codeId)) {
			CodeLabour codeLabour = null;
			// check if labour with given codeId is active
			synchronized (activeCodeLabours) {
				if (!activeCodeLabours.containsKey(codeId)) {
					codeLabour = new CodeLabour(codeId);
					activeCodeLabours.put(codeId, codeLabour);
				}
			}
			if (codeLabour != null) {
				codeLabour.start();
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}

	public double getCodeProgress(int codeId) {
		CodeLabour codeLabour = activeCodeLabours.get(codeId);
		if (codeLabour == null) {
			// TODO: 4to delat?
			return 1;
		} else {
			return codeLabour.getProgress();
		}
	}

	public static final AlgoRunManager getManager() throws SQLException {
		if (instance == null) {
			List<String> algoClassNames = new ArrayList<String>();
			 algoClassNames
			 .add(apii.algorithm.fingerprints.FingerprintsAlgorithm.class
			 .getCanonicalName());
			algoClassNames.add(apii.algorithm.colmo.ColmogorovDiffAlgo.class
					.getCanonicalName());
			// algoClassNames
			// .add(apii.algorithm.TestAlgo.class.getCanonicalName());
			for (String ss : algoClassNames) {
				System.out.println("in list: " + ss);
			}
			instance = new AlgoRunManager(algoClassNames);
		}

		return instance;
	}

	private boolean isCodeChecked(int codeId) throws SQLException {
		try (Connection c = ServerThings.getThings().getConnection();
				PreparedStatement s = c
						.prepareStatement("select codes.checked from codes where codes.code_id = ?");) {
			s.setInt(1, codeId);
			ResultSet rs = s.executeQuery();
			rs.next();
			return rs.getBoolean(1);
		}
	}

	private AlgoRunManager(List<String> algoClassNames) throws SQLException {
		try {
			algos = new Algorithm[algoClassNames.size()];
			for (int i = 0; i < algos.length; i++) {
				algos[i] = (Algorithm) Class.forName(algoClassNames.get(i))
						.getConstructor().newInstance();
				System.out.println("constructing algo: "
						+ algos[i].getClass().getName());
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		runCodeLabours();
	}

	private void runCodeLabours() throws SQLException {

		// TODO: move ids from connection to list in order to give connection
		// back to the pool
		try (Connection c = ServerThings.getThings().getConnection();
				Statement s = c.createStatement();) {
			// TODO: prepared statement
			ResultSet rs = s.executeQuery("SELECT apii.codes.code_id "
					+ "FROM apii.codes " + "WHERE apii.codes.checked = false");
			while (rs.next()) {
				int codeId = (Integer) rs.getObject(1);
				runCodeLabour(codeId);
			}
		}
	}

	private class ProgressMonitorImpl implements ProgressMonitor {
		private int codeId;
		private String algoName;
		private volatile double progress;

		private static final double MINIMAL_STEP = 0.01;

		public ProgressMonitorImpl(int codeId) {
			this.codeId = codeId;
		}

		@Override
		public void setProgress(double progress) {
			if (progress > this.progress + MINIMAL_STEP) {
				this.progress = progress;
			}
		}

		public double getProgress() {
			return progress;
		}
	}

	/**
	 * TODO: remake
	 * 
	 * @author evgeniy
	 * 
	 */
	private class CodePretendentIterator implements Iterator<Integer> {

		private int i = 0;
		private int lastCodeId;

		private CodePretendentIterator(int codeId) {
			this.lastCodeId = codeId - 1;
		}

		@Override
		public boolean hasNext() {
			return i < lastCodeId;
		}

		@Override
		public Integer next() {
			if (!hasNext()) {
				throw new IllegalStateException();
			}
			i++;
			return i;
		}

		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * <b>Note:</b> Thread UNSAFE.
	 * <p>
	 * <b>Note:</b> Doesn't create threadz
	 * 
	 * @author evgeniy
	 */
	private class CodeLabour {
		public ProgressMonitorImpl[] pMonitors;
		private AlgoThread[] algoThreads;
		private int codeId;
		private Thread runner;
		Connection c;

		public CodeLabour(int codeId) throws SQLException {
			this.codeId = codeId;
			runner = new Thread() {
				public void run() {
					try {
						onStartThread();
					} catch (SQLException e) {
						// никак не перехватить
						e.printStackTrace();
						synchronized (activeCodeLabours) {
							activeCodeLabours.remove(CodeLabour.this);
						}
					}
				}
			};
		}

		public void start() {
			runner.start();
		}

		public double getProgress() {
			double result = 0;
			for (int i = 0; i < pMonitors.length; i++) {
				result += pMonitors[i].getProgress();
			}
			result /= pMonitors.length;
			return result;
		}

		private void onStartThread() throws SQLException {
			c = ServerThings.getThings().getConnection();
			pMonitors = new ProgressMonitorImpl[algos.length];
			algoThreads = new AlgoThread[algos.length];
			for (int i = 0; i < algos.length; i++) {
				pMonitors[i] = new ProgressMonitorImpl(codeId);
				algoThreads[i] = new AlgoThread(codeId, algos[i],
						new CodePretendentIterator(codeId), pMonitors[i], c,
						this);
			}
			for (int i = 0; i < algoThreads.length; i++) {
				algoThreads[i].start();
			}
		}

		/**
		 * Method is called from all the algorithm threads
		 */
		private void onThreadFinished(AlgoThread invoker, boolean success) {

			boolean someThreadFailed = false;
			boolean someThreadRunning = false;

			synchronized (this) {
				if (success) {
					invoker.threadState = FINISHED_OK;
				} else {
					invoker.threadState = FINISHED_FAIL;
				}
				for (int i = 0; i < algoThreads.length; i++) {
					if (algoThreads[i].threadState == FINISHED_FAIL) {
						someThreadFailed = true;
					}
					if (algoThreads[i].threadState == RUNNING) {
						someThreadRunning = true;
					}
				}
			}

			if (!someThreadRunning) {
				try {
					if (!someThreadFailed) {
						PreparedStatement s = c
								.prepareStatement("update codes set codes.checked = true "
										+ "where codes.code_id = ?");
						s.setInt(1, codeId);
						System.out.println("commiting results for code id: "
								+ codeId);
						s.close();
						c.commit();
					} else {
						System.out.println("rolling back results for code id: "
								+ codeId);
						c.rollback();
					}
				} catch (SQLException ex) {
					ex.printStackTrace();
				} finally {
					try {
						System.out.println("closing connection: " + codeId);
						c.close();
					} catch (SQLException e) {
						e.printStackTrace();
					} finally {
						System.out.println("remove from activeCodeLabours: "
								+ codeId);
						synchronized (activeCodeLabours) {
							activeCodeLabours.remove(codeId);
						}
						System.out.println("removed from activeCodeLabours: "
								+ codeId);
					}
				}
			}
		}
	}

	private class AlgoThread extends Thread {
		private int codeId;
		private Algorithm algo;
		private Iterator<Integer> iterator;
		private ProgressMonitor pm;
		private Connection c;
		private CodeLabour codeLabour;
		public int threadState;

		public AlgoThread(int codeId, Algorithm algo,
				Iterator<Integer> iterator, ProgressMonitor pm, Connection c,
				CodeLabour codeLabour) {
			this.algo = algo;
			this.codeId = codeId;
			this.iterator = iterator;
			this.pm = pm;
			this.c = c;
			this.codeLabour = codeLabour;
			this.threadState = RUNNING;
		}

		public void run() {
			try {
				System.out.println("thread for id: " + codeId + "; algo: "
						+ algo.getClass().getName() + " start");
				algo.inspect(c, codeId, iterator, pm);
				System.out.println("thread for id: " + codeId + "; algo: "
						+ algo.getClass().getName() + " end");
				codeLabour.onThreadFinished(this, true);
			} catch (SQLException e) {
				System.out.println("thread for id: " + codeId + "; algo: "
						+ algo.getClass().getName() + " fail");
				codeLabour.onThreadFinished(this, false);
				e.printStackTrace();
			} finally {
			}
		}
	}
}